package light3d;

import javax.swing.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.util.*;
/**
 * The backbone of the 3D engine, BufferedCanvas handles the actual rendering of 3D objects
 * 
 * @author Daniel Church
 * @version 1.0
 * @since 1.0
 */
public class BufferedCanvas extends JFrame
{
  TriangleList triangleList, currentTriangle;

	private BufferedImage colorBuffer;
	protected int bufferWidth, bufferHeight, bufferScale;

	protected int clearColor;
	private double farClip, nearClip;
	private boolean isRendering = false;

	private long renderStartTime = 0;
	private double recordedRenderTime = 0;
	
	int numTriangles = 0;
	
	long totalTriangles = 0, totalTime = 0, totalFrames = 0;
	
	public boolean wireFrameOnly = false;
	public boolean drawBackFaces = true;
	public boolean enableLighting = true;
	public double lightPercent = 0.5;
	
	private Graphics2D cGraphics2d;
	
	/**
	 * Creates a new BufferedCanvas, used to perform 3d rendering
	 * 
	 * @param width The desired width of the 2d viewport
	 * @param height The desired height of the 2d viewport
	 * @param cR The Red Component of the background color
	 * @param cG The Blue Component of the background color
	 * @param cB The Green Component of the background color
	 * @param maxDist The maximum distance at which objects will be rendered
	 * @since 1.0
	 */
	public BufferedCanvas(String title, int width, int height, int cR, int cG, int cB, double maxDist)
	{
		super(title);
		
		FastMath.initTables();
		
		bufferWidth = width;
		bufferHeight = height;
		bufferScale = height;

		colorBuffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		cGraphics2d = colorBuffer.createGraphics();
		clearColor = BufferedCanvas.getRGBColor(cR, cG, cB);

		farClip = maxDist;
		nearClip = 0.0;
	}

	/**
	 * Clears information from the previous render, and begins a new rendering cycle.  Must be called before rendering geometry.
	 * 
	 * @see endRender
	 * @since 1.0
	 */
	public void beginRender()
	{
    triangleList = new TriangleList();
    currentTriangle = triangleList;
		numTriangles = 0;
		renderStartTime = System.currentTimeMillis(); 
		isRendering = true;
		cGraphics2d.setColor(new Color(clearColor));
		cGraphics2d.fillRect(0, 0, bufferWidth, bufferHeight);
	}
	
	/**
	 * Completes the render cycle, and displays the results on the screen
	 *
	 * @see beginRender
	 * @since 1.0
	 */
	public double endRender()
	{
    TriangleList triangleArray[] = new TriangleList[numTriangles];
    triangleList = triangleList.getNext();

    {
      int i = 0;
      while (triangleList != null)
      {
        triangleArray[i++] = triangleList;
        triangleList = triangleList.getNext();
      }
    }
		perform2DOperations(cGraphics2d);

    java.util.List<TriangleList> triangles = Arrays.asList(triangleArray);
    Collections.sort(triangles);

    int[] x = new int[3];
    int[] y = new int[3];

    for (int i = numTriangles - 1; i >= 0; i--)
    {
      cGraphics2d.setPaint(new Color(triangles.get(i).getColor(), false));

      TriangleList t = triangles.get(i);

      x[0] = (int)t.getX(0);
      x[1] = (int)t.getX(1);
      x[2] = (int)t.getX(2);

      y[0] = (int)t.getY(0);
      y[1] = (int)t.getY(1);
      y[2] = (int)t.getY(2);
      cGraphics2d.fillPolygon(x, y, 3);
    }

		
		isRendering = false;
		repaint();
		long frameTime = System.currentTimeMillis() - renderStartTime;
		
		totalTriangles += numTriangles;
		totalTime += frameTime;
		totalFrames++;
		
		recordedRenderTime = frameTime/1000.0;
		return recordedRenderTime;
	}
	/**
	 * Renders a list of triangles, must be called between beginRender and endRender for triangles to appear. 
	 * If you wish to deal with complex geometry, it is instead recomended that you use the BasicMesh derived class,
	 * which will provide advanced facilities such as polygon grouping and camera placement.
	 * 
	 * @param triangles An array of triangles to be rendered
	 * @see drawTriangle BasicMesh beginRender endRender triangle
	 * @since 1.0
	 */
	public void renderTriangleList(Triangle[] triangles)
	{
		for (int i = 0; i < triangles.length; i++)
		{
			this.drawTriangle(triangles[i]);
		}
	}
	/**
	 * Renders a single triangle, must be called between beginRender and endRender for triangle to display.
	 * If you wish to deal with complex geometry, it is instead recomended that you use the BasicMesh derived class,
	 * which will provide advanced facilities such as polygon grouping and camera placement. 
	 * 
	 * @param triangle The triangle to be rendered
	 * @see triangle beginRender endRender renderTriangleList BasicMesh
	 * @since 1.0
	 */
	public void drawTriangle(Triangle triangle)
	{
		Boolean isGood = true;
		for (int i = 0; i < 3; i++)
		{
			triangle.p[i].rotate(triangle.getRX(), triangle.getRY(), triangle.getRZ());
			triangle.p[i].translate(triangle.getX(), triangle.getY(), triangle.getZ());
		}
		if (triangle.getZ() > nearClip)
		{
			int isVisible = 0;
			for (int i = 0; i < 3; i++)
			{
				isGood &= triangle.p[i].projectPoint(nearClip, farClip);
				triangle.p[i].scaleByScreen(bufferScale, -bufferHeight);
				triangle.p[i].translate(bufferWidth/2.0,bufferHeight/2.0,0.0);
	
				if (triangle.p[i].getX() > 0 && triangle.p[i].getX() < bufferWidth && triangle.p[i].getY() > 0 && triangle.p[i].getY() < bufferHeight);
				{
					isVisible++;
				}
			}
			if (isGood && (isVisible==3))
			{
				numTriangles++;
				drawBufferedTriangle(triangle, triangle.color);
			}
		}
		
		for (int i = 0; i < 3; i++)
		{
			triangle.p[i].resetChanges();
		}
	}
	protected Point3D transformPoint(Point3D point3d)
	{
		point3d.projectPoint(nearClip, farClip);
		point3d.scaleByScreen(bufferScale, -bufferHeight);
		point3d.translate(bufferWidth/2.0, bufferHeight/2.0, 0.0);
		
		return point3d;
	}
	private void drawBufferedTriangle(Triangle triangle, int color)
	{
    double[] zBack = new double[3]; 
		for (int i = 0; i < 3; i++)
		{
      zBack[i] = triangle.p[i].getZ();
		}
    Arrays.sort(zBack);

    double lZ = (1.0 + triangle.lightNormal.getZ())/2.0;
    int c = multiplyColor(triangle.color, lZ * lightPercent + (1 - lightPercent));
    TriangleList tmp = new TriangleList(triangle.p[0].getX(), triangle.p[0].getY(),
      triangle.p[1].getX(), triangle.p[1].getY(),
      triangle.p[2].getX(), triangle.p[2].getY(), zBack[0], zBack[1], zBack[2], c);
    currentTriangle.giveNext(tmp);
    currentTriangle = tmp;
	}
	private static int blendColor(int c1, int c2, double blendPercent)
	{
		int r1 = ((c1 >> 16) & 0xff),
		g1 = ((c1 >> 8) & 0xff),
		b1 = ((c1 ) & 0xff);

		int r2 = ((c2 >> 16) & 0xff),
		g2 = ((c2 >> 8) & 0xff),
		b2 = ((c2 ) & 0xff);

		r1 = (int)((r2 - r1) * blendPercent + r1);
		g1 = (int)((g2 - g1) * blendPercent + g1);
		b1 = (int)((b2 - b1) * blendPercent + b1);
		return (r1 << 16) | (g1 << 8) | b1;
	}
	private static int multiplyColor(int c1, int c2)
	{
		int r1 = ((c1 >> 16) & 0xff),
		g1 = ((c1 >> 8) & 0xff),
		b1 = ((c1 ) & 0xff);

		int r2 = ((c2 >> 16) & 0xff),
		g2 = ((c2 >> 8) & 0xff),
		b2 = ((c2 ) & 0xff);

		r1 = r1 * r2 / 255;
		g1 = g1 * g2 / 255;
		b1 = b1 * b2 / 255;
		return (r1 << 16) | (g1 << 8) | b1;
	}
	public static int multiplyColor(int c1, double x)
	{
		int r1 = ((c1 >> 16) & 0xff),
		g1 = ((c1 >> 8) & 0xff),
		b1 = ((c1 ) & 0xff);
		
		r1 = (int)((double)r1 * x);
		g1 = (int)((double)g1 * x);
		b1 = (int)((double)b1 * x);
		
		return (r1 << 16) | (g1 << 8) | b1;
	}
	
	private static double blendU(Point3D a, Point3D b, double blendPercent)
	{
		return a.getU() + (b.getU() - a.getU()) * blendPercent;
	}
	private static double blendV(Point3D a, Point3D b, double blendPercent)
	{
		return a.getV() + (b.getV() - a.getV()) * blendPercent;
	}
	/**
	 * Takes the components r, g, b, and returns a single 32-bit integer holding the RGB color value
	 * 
	 * @param r The red component (0-255)
	 * @param g The green component (0-255)
	 * @param b The blue component (0-255)
	 * @return The color's integer representation
	 * @since 1.0
	 */
	public static int getRGBColor(int r, int g, int b)
	{
		return (r << 16) | (g << 8) | b;
	}
	
	public double getFrameTime()
	{
		return recordedRenderTime;
	}
	
	public void printStatistics()
	{
		System.out.println("FPS\tFRAMES\tTRIANGLES/FRAME\tTIME\tTRI/SEC");
		if (totalTime==0 || totalTriangles==0 || totalFrames==0)
		{
			System.out.println("NOTHING RENDERED");
			return;
		}
		
		System.out.println((int)((double)totalFrames * 1000.0 / totalTime) + "\t" + totalFrames + "\t" + totalTriangles/totalFrames + "\t\t" + totalTime/1000.0 + "\t" + (totalTriangles*1000/totalTime));
		totalTime = 0;
		totalFrames = 0;
		totalTriangles = 0;
	}
	public void update(Graphics g)
	{
		paint(g);
	}
	public void paint(Graphics g)
	{
		if (isRendering)
		{
			return;
		}
		Graphics2D graphics2d = (Graphics2D) g;
		graphics2d.drawImage(colorBuffer, 0, 0, null);
	}
	public void perform2DOperations(Graphics2D graphics2d)
	{
		graphics2d.setColor(new Color(clearColor));
		graphics2d.fillRect(0, 0, bufferWidth, bufferHeight);
	}
}
